Webpack 学习笔记

birthday-celebration-gift-packaging

Webpack 的核心设计理念

一切皆模块

Webpack 通过 loader 来处理其他非 .js 文件,比如 HTML 和 CSS 文件,字体文件,图片。以上这些全都被当做模块来看待,依赖关系更加明确清晰,也更适合复用。

只在需要的时候加载需要的模块

如果最后打包输出的是一个 app.bundle.js 文件,这个文件的体积可能很大,用户访问应用程序的首页时需要等待其被全部加载、编译、执行,这个等待时间会很长。更合理的做法是,优先加载首页渲染所需要的模块,其他的资源在浏览器主线程空闲和网络空闲的时候延迟加载。只在需要的时候加载需要的模块,除了延迟部分模块的加载,还可以做以下优化:

  • 抽取使用次数较多的公共模块,进一步缩减打包后的体积
  • 抽取代码更新频率较低的第三方库,额外打包以利用浏览器的缓存
  • 去除无副作用的 dead code。所谓 dead code 是指既没有 export 模块,也没有像 polyfill 类工具库那样做影响全局作用域的处理(比如添加 Promisewindow 对象上)。去除这些 dead code 不会对整个应用程序产生影响。这一过程即所谓的 Tree Shaking。
  • 利用 prefetching 特性确保延迟加载的模块不至于影响用户体验

术语表:

  • Code splitting 代码分割
  • Lazy loading 懒加载
  • Prefetching 预获取
  • code splitting 代码分割

Webpack 中有不少功能可以较为显著地提升网页的性能。下面列举出其中几个:

代码分割与懒加载、预获取

如果不使用代码分割,那么 Webpack 打包出来的只有一个类似 app.bundle.js 的文件。这有什么问题呢?主要有以下几个方面的缺陷,可能导致首页加载速度变慢:

  1. 代码中使用的诸如 lodash 第三方库,它们通常没有我们自己的应用代码更新地那么频繁。为了利用浏览器的缓存机制,可以将第三方库的代码独立出来,另外打包。这样,浏览器如果检测到第三方库的文件没有变动,便不会发送 http 请求,而是直接从缓存中读取,这样在刷新页面或者第二次进入的时候可以加快网页加载的速度。
  2. 包含了许多首页不需要的代码,比如弹窗、模态框这些交互性组件,比如详情页面,比如首页不需要的 CSS 样式,比如首页不需要的其他 JavaScript 代码等。这会拖慢首页的加载速度,降低用户体验。ES6 的 import 语法返回的是一个 Promise 的实例,可以利用这一点实现异步加载代码、模块。Webpack 在打包过程中会自动识别这样的 import 语法,将这些异步代码进行分割以实现延迟加载,这也是 Webpack 代码分割默认的匹配块是 async 的原因。以上所说的就是懒加载,当然,具体哪些资源用懒加载处理,需要开发者自己来按需配置。
  3. 上面提到的懒加载方式,在某些时候需要配合预获取来使用。举个例子,假如某个业务庞大的网站,首页仅加载了一些关键路径的资源,类似详情页面这类组件被设置为当用户点击后才会去加载。但如果详情页面组件本身体积很大,那么可能在用户点击几秒之后才会到达,这是很影响用户体验的。所以,理想的情况是,当首页加载完毕,主线程空闲了,网络请求也空闲了,再在后台获取这些体积较大的异步组件或其他资源(实际上,只要用户大概率会使用到、但是不影响首页的模块都应该采用懒加载再配合预获取的手段来处理)。Webpack 的 prefetch/preloading 特性可以解决这一问题。

我们以 Webpack 官网上一个例子来说明 prefetching 特性:现在我们有一个 HomePage 组件,其中包含一个 LoginButton 的子组件(该组件会在首页加载时渲染)。LoginButton 组件在被点击后会加载 LoginModal 组件。为了更好的用户体验,我们应该预获取 LoginModal 组件。在 LoginButton.js 组件内,通过在引入 LoginModal 组件时增加特殊的注释以让 Webpack 识别:

1
import(/* webpackPrefetch: true */ 'LoginModal')

Webpack 见到这行代码后,会将 <link rel="prefetch" href="login-modal-chunk.js"> 添加到 HTML 文件的 <head> 标签内。这会指示浏览器在空闲时段加载 login-modal-chunk.js 文件。

Tree Shaking

Tree Shaking 指去除那些在 JavaScript 上下文中未引用到的代码,这一特性是基于 ES2015 的静态结构特性。考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
// math.js
export function square(x) {
return x * x
}

export function cube(x) {
return x * x * x
}

// index.js
import { cube } from './math.js'
console.log(cube(3))

上面代码中,index.js 模块没有导入 square 函数,为了更一步缩减加载到浏览器端的脚本大小,应该去除类似这里 square 函数的未被引用的代码。Webpack 官网上指明,要实现 tree shaking,需要满足以下 3 个条件:

  1. 使用 ES2015 模块语法(importexport
  2. 在项目 package.json 文件中,添加一个 sideEffects 属性
  3. 引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如 UglifyJSPlugin

按我自己的操作,目前来看,第 2 步不是必须的,在生产模式下未指定 sideEffects 也可以。如果使用生产模式,第 3 步 Webpack 也会默认引入 UglifyJSPlugin 压缩工具。简而言之,在生产模式下,你只需要使用 ES2015 的模块语法即可实现 tree shaking。

参考